<script lang="tsx">
import { useEventListener } from "@/hooks/useEventListener";

type NumberOrNumberString = PropType<string | number | undefined>;

const props = {
  height: [Number, String] as NumberOrNumberString,
  maxHeight: [Number, String] as NumberOrNumberString,
  maxWidth: [Number, String] as NumberOrNumberString,
  minHeight: [Number, String] as NumberOrNumberString,
  minWidth: [Number, String] as NumberOrNumberString,
  width: [Number, String] as NumberOrNumberString,
  bench: {
    type: [Number, String] as NumberOrNumberString,
    default: 0
  },
  itemHeight: {
    type: [Number, String] as NumberOrNumberString,
    required: true
  },
  items: {
    type: Array,
    default: () => []
  }
};

const prefixCls = "virtual-scroll";

function convertToUnit(str: string | number | null | undefined, unit = "px"): string | undefined {
  if (str == null || str === "") {
    return undefined;
  } else if (isNaN(+str!)) {
    return String(str);
  } else {
    return `${Number(str)}${unit}`;
  }
}

export default defineComponent({
  name: "VirtualScroll",
  props,
  setup(props, { slots }) {
    const wrapElRef = ref<HTMLDivElement | null>(null);
    const state = reactive({
      first: 0,
      last: 0,
      scrollTop: 0
    });

    //默认渲染dom的个数
    const getBenchRef = computed(() => {
      return parseInt(props.bench as string, 10);
    });

    const getItemHeightRef = computed(() => {
      return parseInt(props.itemHeight as string, 10);
    });

    const getFirstToRenderRef = computed(() => {
      return Math.max(0, state.first - unref(getBenchRef));
    });

    const getLastToRenderRef = computed(() => {
      return Math.min((props.items || []).length, state.last + unref(getBenchRef));
    });

    const getContainerStyleRef = computed((): CSSProperties => {
      return {
        height: convertToUnit((props.items || []).length * unref(getItemHeightRef))
      };
    });

    const getWrapStyleRef = computed((): CSSProperties => {
      const styles: Record<string, any> = {};
      const height = convertToUnit(props.height);
      const minHeight = convertToUnit(props.minHeight);
      const minWidth = convertToUnit(props.minWidth);
      const maxHeight = convertToUnit(props.maxHeight);
      const maxWidth = convertToUnit(props.maxWidth);
      const width = convertToUnit(props.width);

      if (height) styles.height = height;
      if (minHeight) styles.minHeight = minHeight;
      if (minWidth) styles.minWidth = minWidth;
      if (maxHeight) styles.maxHeight = maxHeight;
      if (maxWidth) styles.maxWidth = maxWidth;
      if (width) styles.width = width;
      return styles;
    });

    watch([() => props.itemHeight, () => props.height], () => {
      onScroll();
    });

    function getLast(first: number): number {
      const wrapEl = unref(wrapElRef);
      if (!wrapEl) {
        return 0;
      }
      const height = parseInt(String(props.height) ?? 0, 10) || wrapEl.clientHeight;

      return first + Math.ceil(height / unref(getItemHeightRef));
    }

    function getFirst(): number {
      return Math.floor(state.scrollTop / unref(getItemHeightRef));
    }

    function onScroll() {
      const wrapEl = unref(wrapElRef);
      if (!wrapEl) {
        return;
      }
      state.scrollTop = wrapEl.scrollTop;
      state.first = getFirst();
      state.last = getLast(state.first);
    }

    function renderChildren() {
      const { items = [] } = props;
      return items.slice(unref(getFirstToRenderRef), unref(getLastToRenderRef)).map(genChild);
    }

    function genChild(item: any, index: number) {
      index += unref(getFirstToRenderRef);
      const top = convertToUnit(index * unref(getItemHeightRef));
      return (
        <div class={`${prefixCls}__item`} style={{ top }} key={index}>
          {slots["default"]!({ item, index })}
        </div>
      );
    }

    onMounted(() => {
      state.last = getLast(0);
      nextTick(() => {
        const wrapEl = unref(wrapElRef);
        if (!wrapEl) {
          return;
        }
        useEventListener({
          el: wrapEl,
          name: "scroll",
          listener: onScroll,
          wait: 0
        });
      });
    });

    return () => (
      <div class={prefixCls} style={unref(getWrapStyleRef)} ref={wrapElRef}>
        <div class={`${prefixCls}__container`} style={unref(getContainerStyleRef)}>
          {renderChildren()}
        </div>
      </div>
    );
  }
});
</script>

<style scoped lang="scss">
@import "./index.scss";
</style>
